package cn.felord.verse.configure;

import cn.felord.security.autoconfigure.jackson2.SecurityJackson2Module;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.jackson2.SecurityJackson2Modules;

import java.time.Duration;
import java.util.EnumSet;
import java.util.stream.Collectors;

/**
 * The type Spring redis cache configuration.
 *
 * @author n1
 * @since 2021 /4/12 16:57
 */
@EnableCaching
@Configuration(proxyBeanMethods = false)
public class SpringRedisCacheConfiguration {

    /**
     * Redis template.
     *
     * @param redisConnectionFactory the redis connection factory
     * @return the redis template
     */
    @Bean
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = initJacksonSerializer();
        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * Redis cache configuration.
     *
     * @param redisTemplate   the redis template
     * @param cacheProperties the cache properties
     * @return the redis cache configuration
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(RedisTemplate<Object, Object> redisTemplate, CacheProperties cacheProperties) {
        // 参见 spring.cache.redis
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 缓存的序列化问题
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(redisTemplate.getValueSerializer()));

        if (redisProperties.getTimeToLive() != null) {
            // 全局 TTL 时间
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            // key 前缀值
            redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            // 默认缓存null值 可以防止缓存穿透
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            // 不使用key前缀
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }
        return redisCacheConfiguration;
    }


    /**
     * Redis cache manager 个性化配置缓存过期时间.
     *
     * @param redisCacheConfiguration the redis cache configuration
     * @return the redis cache manager builder customizer
     * @see CacheNameEnum
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisCacheConfiguration redisCacheConfiguration) {

        return builder -> builder.cacheDefaults(redisCacheConfiguration)
                // 自定义的一些缓存配置初始化 主要是特定缓存及其ttl时间
                .withInitialCacheConfigurations(EnumSet.allOf(CacheNameEnum.class).stream()
                        .collect(Collectors.toMap(CacheNameEnum::cacheName,
                                cacheEnum -> redisCacheConfiguration.entryTtl(Duration.ofSeconds(cacheEnum.ttlSecond())))));
    }

    /**
     * The enum Cache name enum.
     */
    public enum CacheNameEnum {

        /**
         * 企业微信TOKEN
         */
        QYWX_TOKEN_CACHE("token::qywx", 7100),
        /**
         * 企业微信 corpTicket
         */
        QYWX_CORP_TICKET_CACHE("ticket::qywx::corp", 7100),
        /**
         * 企业微信 agentTicket
         */
        QYWX_AGENT_TICKET_CACHE("ticket::qywx::agent", 7100);
        /**
         * 缓存名称
         */
        private final String cacheName;
        /**
         * 缓存过期秒数
         */
        private final int ttlSecond;

        CacheNameEnum(String cacheName, int ttlSecond) {
            this.cacheName = cacheName;
            this.ttlSecond = ttlSecond;
        }


        /**
         * Cache name string.
         *
         * @return the string
         */
        public String cacheName() {
            return this.cacheName;
        }


        /**
         * Ttl second int.
         *
         * @return the int
         */
        public int ttlSecond() {
            return this.ttlSecond;
        }
    }

    private Jackson2JsonRedisSerializer<Object> initJacksonSerializer() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        //bugFix Jackson2反序列化数据处理LocalDateTime类型时出错
        om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        om.registerModules(new Jdk8Module(),
                        new JavaTimeModule(),
                        new SecurityJackson2Module())
                .registerModules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader()));
        return new Jackson2JsonRedisSerializer<>(om, Object.class);
    }
}
